package org.windowkit.toolkit.widget.table;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.table.AbstractTableModel;

import org.ddevil.data.ChangeTracker;
import org.ddevil.data.Data;
import org.ddevil.data.DisplayDescriptor;
import org.ddevil.data.Filter;
import org.ddevil.data.RegexFilter;
import org.ddevil.data.SortOrder;
import org.ddevil.data.ChangeTracker.Change;
import org.ddevil.data.set.BasicDataSet;
import org.ddevil.data.set.BasicSortableDataSet;
import org.ddevil.data.set.DataSet;
import org.ddevil.data.set.SortableDataSet.SortInfo;
import org.ddevil.data.util.DataUtils;
import org.windowkit.toolkit.widget.table.event.TableEditedEvent;
import org.windowkit.toolkit.widget.table.event.TableEditedListener;


/**
 * A table model that wraps a DataSet.
 * Uses {@link BasicSortableDataSet} to wrap the DataSet given
 * as our model to support sorting/filtering and implement the
 * {@link SortableTableModel} interface.
 *
 * @author Rich O'Connell
 * @author Eric lundine
 *
 */
public class RecordTableModel extends AbstractTableModel implements SortableTableModel {

    /**
	 *
	 */
	private static final long serialVersionUID = 7073157499870597676L;

	static final int DEFAULT_VISIBLE_ROWS = 10;

    protected BasicSortableDataSet<Data> model;

    protected DisplayDescriptor labels;

    protected int visibleRows;

    protected boolean isSorting = true;
    protected boolean isFiltering = true;

    /** Store the dbids of the editable columns. */
    private ArrayList<String> editableColumns;

    /** Store the TableEditedListenrs */
    protected ArrayList<TableEditedListener> editListeners;

    /** Holds the type of comparator to use for each class type. */
    protected HashMap<Class, Comparator> classComparators;

    /** Holds the type of comparator to use for each column. */
    protected HashMap<String, Comparator> attributeComparators;


    /**
     * Logs changes made through this model.
     * The contract used internally is that if this field is null
     * then change tracking is turned off, if it is non-null then
     * it is turned on.
     * By default this is off.
     */
    protected ChangeTracker changeTracker;


    public RecordTableModel(final DataSet<Data> model, final DisplayDescriptor labels){
        this(model, labels, DEFAULT_VISIBLE_ROWS);
    }

    public RecordTableModel(final DataSet<Data> model, final DisplayDescriptor labels, final int visibleRows) {
        if(model == null){
            throw new IllegalArgumentException(getClass().getSimpleName() +
                    " must supply non-null BasicSortableDataSet.");
        }
        if(labels == null){
            throw new IllegalArgumentException(getClass().getSimpleName() +
                    " must supply non-null DisplayDescriptor.");
        }

        setData(model);
        setLabels(labels);
        setVisibleRows(visibleRows);
    }

    /*
     *
     * {@inheritDoc}
     */
    public void addTableEditedListener(final TableEditedListener l){
        if(editListeners == null) {
			editListeners = new ArrayList<TableEditedListener>();
		}
        editListeners.add(l);
    }

    /*
     *
     * {@inheritDoc}
     */
    public void removeTableEditedListener(final TableEditedListener l) {
        if(editListeners != null) {
			editListeners.remove(l);
		}
    }

    /**
     * Dispatch a TableEditedEvent
     *
     * @param row
     *          The row of the edit.
     * @param col
     *          The column of the edit.
     * @param oldVal
     *          The old value.
     * @param newVal
     *          The new value.
     */
    protected void fireTableEditedEvent(final int row, final int col, final Object oldVal, final Object newVal){
        if(editListeners != null){
            TableEditedEvent e = new TableEditedEvent(this, row, col);
            e.setNewValue(newVal);
            e.setOldValue(oldVal);
            for(TableEditedListener l : editListeners) {
				l.tableEdited(e);
			}
        }
    }

    /*
     *
     * {@inheritDoc}
     */
    public boolean getIsRowSelectable(final int row) {
        return convertViewToModel(row) > -1;
    }




    /**
     * {@inheritDoc}
     */
    public int getRowCount() {
        int rc = Math.max(getVisibleRows(), getDataRowCount());
        if(isFiltering){
            //allways add one for the filter row
            rc++;
        }
        return rc;
    }

    /*
     *
     * {@inheritDoc}
     */
    public int getDataRowCount() {
        return (model != null ? model.size() : 0);
    }

    /*
     *
     * {@inheritDoc}
     */
    public int getVisibleRows() {
        return this.visibleRows;
    }

    /*
     *
     * {@inheritDoc}
     */
    public void setVisibleRows(final int i) {
        this.visibleRows = i;
        fireTableStructureChanged();
    }

    /**
     * {@inheritDoc}
     */
    public int getColumnCount() {
        return (labels != null ? labels.size() : 0);
    }

    /*
     *
     * {@inheritDoc}
     */
    @Override
    public String getColumnName(final int column) {
        if(labels != null){
            return labels.getLabel(labels.getDbIdAtIndex(column));
        }else{
            return super.getColumnName(column);
        }
    }

    /*
     *
     * {@inheritDoc}
     */
    @Override
    public Class<?> getColumnClass(final int columnIndex) {
        Class ret = Object.class;
        if(getDataRowCount() > 0){
            Object o;
            int startAt = isFiltering ? 1 : 0;
            //attempt to find a row with a non null value
            for(int i = startAt; i < getDataRowCount(); i++){
                o = getValueAt(i, columnIndex);
                if(o != null){
                    ret = o.getClass();
                    break;
                }
            }
        }
        return ret;
    }

    /*
     *
     * {@inheritDoc}
     */
    @Override
    public boolean isCellEditable(int rowIndex, final int columnIndex) {
        //filter row is allways editable
        if(isFilterable() && rowIndex == 0){
            return true;
        }
        rowIndex = convertViewToModel(rowIndex);
        //check if the row has actual data behind it
        if(rowIndex < 0 || rowIndex >= getDataRowCount()){
            return false;
        }
        //check our list of editable columns
        return isColumnEditable(getDataBaseID(columnIndex));
    }

    /**
     * {@inheritDoc}
     */
    public Object getValueAt(int rowIndex, final int columnIndex) {
        Object ret = null;
        String dbid = labels.getDbIdAtIndex(columnIndex);

        if(isFilterable() && rowIndex == 0){
            //this is safe since we are the only one applying filters
            Filter<Object> f = model.getFilter(dbid);
            ret = f != null ? f.toString() : null;
        }else{
            rowIndex = convertViewToModel(rowIndex);
            if(rowIndex > -1){
                ret = model.getDataAt(rowIndex, dbid);
            }
        }
        return ret;
    }

    /*
     *
     * {@inheritDoc}
     */
    @Override
    public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) {
        String dbid = getDataBaseID(columnIndex);
        if(isFilterable() && rowIndex == 0){
            //this is a filter being set
        	setFilter(columnIndex, (aValue != null ? aValue.toString() : ""));
        }else{
            int ri = convertViewToModel(rowIndex);

            Object oldVal = model.getDataAt(ri, dbid);

            if(isChangeTracking()){
                //log the changes
                Data old = model.exportRecord(ri);
                model.setDataAt(ri, dbid, aValue);
                Data nu = model.exportRecord(ri);
                changeTracker.itemModified(old, nu);

            }else{
                //just make the changes
                model.setDataAt(ri, dbid, aValue);
            }

            fireTableEditedEvent(rowIndex, columnIndex, oldVal, aValue);


            //we kind of have to update everything since the new value could have potentially
            //effected a sort or a filter
            fireTableDataChanged();
        }
    }

    /*
     *
     * {@inheritDoc}
     */
    public void removeRow(final int index) {
        int row = convertViewToModel(index);
        if(isChangeTracking()){
            changeTracker.itemRemoved(model.exportRecord(row));
        }
        model.removeRecord(row);
        fireTableRowsDeleted(index, index);
    }

    /*
     *
     * {@inheritDoc}
     */
    public void clearTableData(final boolean clearSortsFilters) {

        //instead of destroying all the underlying data we give ourself
        //a new, empty data set, with the same attribute set
        BasicDataSet empty = new BasicDataSet(model.getIdentifiers());


        if(isChangeTracking()){
            changeTracker.clear();
        }
        setData(empty, clearSortsFilters);


    }

    /*
     *
     * {@inheritDoc}
     */
    public void setSortable(final boolean isSortable) {
        this.isSorting = isSortable;
    }

    /*
     *
     * {@inheritDoc}
     */
    public boolean isSortable() {
        return this.isSorting;
    }

    /*
     *
     * {@inheritDoc}
     */
    public void setComparator(final Class className, final Comparator myComparator) {
        if(classComparators == null) {
			classComparators = new HashMap<Class, Comparator>();
		}
        classComparators.put(className, myComparator);
        model.setComparator(className, myComparator);

    }

    /*
     *
     * {@inheritDoc}
     */
    public void setComparator(final int colIndex, final Comparator myComparator) {
        String dbid = labels.getDbIdAtIndex(colIndex);
        if(dbid != null){
            setComparator(dbid, myComparator);
        }
    }

    /**
     * Set the comparator for the column with that ID.
     *
     * @param dbId
     *          The DB Id for the column we want.
     * @param comp
     *          The comparator to use.
     */
    public void setComparator(final String dbId, final Comparator comp){
        if(attributeComparators == null) {
			attributeComparators = new HashMap<String, Comparator>();
		}
        attributeComparators.put(dbId, comp);
        model.setComparator(dbId, comp);
    }



    /*
     *
     * {@inheritDoc}
     */
    public void clearAllSorts() {
        model.clearAllSorts();
        fireTableStructureChanged();
    }

    /*
     *
     * {@inheritDoc}
     */
    public void setSort(final int colIndex, final SortOrder type) {
        String dbid = getDataBaseID(colIndex);
        model.setSort(dbid, type);
        fireTableStructureChanged();
    }

    /*
     *
     * {@inheritDoc}
     */
    public SortOrder getSortDirection(final int i) {
        SortInfo info = model.getSortInfo(getDataBaseID(i));
        return (info != null ? info.type : SortOrder.NONE);
    }

    /*
     *
     * {@inheritDoc}
     */
    public int getSortDepth(final int i) {
        SortInfo info = model.getSortInfo(getDataBaseID(i));
        return (info != null ? info.depth : -1);
    }

    /*
     *
     * {@inheritDoc}
     */
    public void setFilterable(final boolean isFilterable) {
        this.isFiltering = isFilterable;
    }

    /*
     *
     * {@inheritDoc}
     */
    public boolean isFilterable() {
        return this.isFiltering;
    }

    /*
     *
     * {@inheritDoc}
     */
    public void setFilter(final int column, final String filter) {
        setFilter(getDataBaseID(column), filter);
    }


    /**
     * {@inheritDoc}
     */
    public void setNegateFilter(final boolean negate) {
    	model.setNegateFilters(negate);
    	fireTableDataChanged();
    }


    /**
     * Apply the given String as a StringFilter for the attribute
     * with the given id.
     *
     * @param dbid
     * @param filter
     */
    public void setFilter(final String dbid, final String filter){
        if(isFilterable()){
            Filter<Object> filt = null;
            if(DataUtils.hasValue(filter)){
                filt = new RegexFilter(filter);//new RegexFilter("^.*" + filter + "$");
            }
            model.setFilter(dbid, filt);
            fireTableDataChanged();
        }
    }

    /**
     * Get the DB Id for the column at the given model index.
     *
     * @param modelIndex
     *          The index of the column to get.
     * @return
     *      The ID for that column.
     */
    public String getDataBaseID(final int modelIndex) {
        return labels.getDbIdAtIndex(modelIndex);
    }

    /**
     * Get the model index for the column with the given DB ID.
     *
     * @param dbId
     *          The ID for the column.
     * @return
     *          The column's model index.
     */
    public int getModelIndex(final String dbId) {
        return labels.getIndexOfId(dbId);
    }

    /**
     * Exports a DataSet representing the records at the
     * given indices.
     *
     * @param selectedRows
     *          An array containing the indices of the records to get.
     * @return
     *      A DataSet containing the requested records.
     */
    public DataSet getRecords(final int[] selectedRows) {
        int[] newIndices = new int[selectedRows.length];
        for (int i = 0; i < newIndices.length; i++) {
            newIndices[i] = convertViewToModel(selectedRows[i]);
        }
        return model.exportRecords(newIndices);
    }

    /**
     * Get the record at the selected index.
     *
     * @param selectedRow
     *          The index of the record to get.
     * @return
     *          An Data object representing the requested row.
     */
    public Data getRecord(final int selectedRow) {
        return model.exportRecord(convertViewToModel(selectedRow));
    }

    /**
     * Add the record to the DataSet that is backing this model.
     *
     * @param newRecord
     *      The record to add.
     */
    public void addRecord(final Data newRecord) {
        if(isChangeTracking()){
            changeTracker.itemAdded(newRecord);
        }
        model.importRecord(newRecord);
        //we really can't tell exactly where the record was added since
        //a sort could be on
        fireTableDataChanged();

    }

    /**
     * Add all the records in the given DataSet to the DataSet that
     * is backing this model.
     *
     * @param records
     *      The records to add.
     */
    public void addRecords(final DataSet records) {
        if(isChangeTracking()){
            for(int i = 0; i < records.size(); i++){
                changeTracker.itemAdded(records.exportRecord(i));
            }
        }

        model.importRecords(records);
        //we really can't tell exactly where the record was added since
        //a sort could be on
        fireTableDataChanged();

    }

    public void setData(final DataSet<Data> data){
        setData(data, false);
    }

    /**
     * Set the DataSet that will be backing this model.
     *
     * @param data
     */
    public void setData(final DataSet<Data> data, final boolean clearSortsAndFilters) {

        BasicSortableDataSet<Data> sds;

        //wrap it up bitch!
        sds = new BasicSortableDataSet<Data>(data);



        // keep the current sorts and filters applied to prevous model
        if (!clearSortsAndFilters && this.model != null) {



            Map<String, Filter<Object>> filters = new HashMap<String, Filter<Object>>();
            List<SortInfo> sorts = new ArrayList<SortInfo>();

             for(String id : model.getIdentifiers()){
                 SortInfo si = model.getSortInfo(id);
                 if(si != null){
                     sorts.add(si);
                 }
                 Filter<Object> filt = model.getFilter(id);
                 if(filt != null) {
					filters.put(id, filt);
				}

             }

             //add to new model

             for(String id : sds.getIdentifiers()){
                 Filter<Object> f = filters.get(id);
                 if(f != null){
                     sds.setFilter(id, f);
                 }
             }

             Collections.sort(sorts, new Comparator<SortInfo>(){

                public int compare(final SortInfo o1, final SortInfo o2) {
                    if(o1.depth > o2.depth) {
						return 1;
					}
                    if(o1.depth < o2.depth) {
						return -1;
					}
                    return 0;
                }
             });

             List<String> newIdList = sds.getIdentifiers();
             for(SortInfo s : sorts){
                 if(newIdList.contains(s.dbIdentifier)){
                     sds.setSort(s.dbIdentifier, s.type);
                 }
             }

        }


        //add any special comparators
        if(attributeComparators != null && attributeComparators.size() > 0){
            for(String dbid :attributeComparators.keySet()){
                sds.setComparator(dbid, attributeComparators.get(dbid));
            }
        }
        if(classComparators != null && classComparators.size() > 0){
            for(Class cls : classComparators.keySet()){
                sds.setComparator(cls, classComparators.get(cls));
            }
        }


        this.model = sds;
        fireTableStructureChanged();
    }

    /**
     * Set the DisplayDescriptor that will define the columns to be
     * displayed by default, the order to use by default, and the labels
     * to use for the DB Ids.  This will clear all current sorts and filters.
     *
     * @param labels2
     */
    public void setLabels(final DisplayDescriptor labels2) {
        this.labels = labels2;
        model.clearAllFilters();
        model.clearAllSorts();
        fireTableStructureChanged();
    }

    /**
     * Convenience method to convert a model index to a view index
     */
    protected int convertModelToView(int i) {
        if(isFilterable()){
            i++;
        }
        return i;
    }

    /**
     * Convenience method to convert a view index to a model index
     */
    protected int convertViewToModel(final int i) {
        int ret = i;
        if(isFilterable()){
            ret--;
        }
        if(ret < 0 || ret >= getDataRowCount()){
            ret = -1;
        }
        return ret;
    }

    public void setColumnEditable(final String dbId, final boolean editable){
        if(editable){
            if(editableColumns == null){
                editableColumns = new ArrayList<String>();
            }
            if(!editableColumns.contains(dbId)){
                editableColumns.add(dbId);
            }
        }else{
            if(editableColumns != null){
                editableColumns.remove(dbId);
            }
        }
    }

    public boolean isColumnEditable(final String dbId){
        boolean ret = false;
        if(editableColumns != null){
            ret =editableColumns.contains(dbId);
        }
        return ret;
    }

    /**
     * Get a list of all the changes that have been made through this
     * model since the last time the change tracker was reset.
     *
     * @return
     *      A List containing a Change object for each change made.
     */
    public List<Change> getChanges(){
        return (changeTracker != null ? changeTracker.getChanges() : null);
    }

    /**
     * Reset the contents of the change tracker.
     */
    public void resetChangeTracker(){
        if(changeTracker != null) {
			changeTracker.clear();
		}
    }

    /**
     * Return whether or not this model is tracking it's changes.
     * By default it is not.
     * Turn on change tracking by calling {@link #trackChanges(boolean)}.
     *
     * @return
     */
    public boolean isChangeTracking(){
        return changeTracker != null;
    }

    /**
     * Turn on or off the change tracker.
     * Change Tracking is off by default.
     *
     * @param track
     *          TRUE to turn on change tracking.
     *          FALSE to turn off change tracking.
     */
    public void trackChanges(final boolean track){
        if(track){
            if(changeTracker == null){
                changeTracker = new ChangeTracker();
            }
        }else{
            changeTracker = null;
        }
    }




}
